/** * Jenga Mortgage Calculator - Frontend JavaScript * * @package Jenga_Mortgage_Calculator */ (function($) { 'use strict'; // Store calculation results for lead capture var calculationResults = {}; var charts = {}; /** * Format number as currency */ function formatCurrency(amount, currencyCode) { var currencies = jmcData.currencies; var symbol = currencies[currencyCode] ? currencies[currencyCode].symbol : currencyCode; return symbol + numberWithCommas(parseFloat(amount).toFixed(2)); } /** * Add commas to number */ function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } /** * Parse currency input (remove formatting) */ function parseCurrencyInput(value) { if (!value) return 0; return parseFloat(value.toString().replace(/[^0-9.-]/g, '')) || 0; } /** * Format input as currency while typing */ function formatCurrencyInput(input) { var value = input.val().replace(/[^0-9.]/g, ''); if (value) { // Handle decimal places var parts = value.split('.'); parts[0] = numberWithCommas(parts[0]); if (parts[1] !== undefined) { parts[1] = parts[1].substring(0, 2); } value = parts.join('.'); } input.val(value); } /** * Update currency symbols when currency changes */ function updateCurrencySymbols(calculator, currencyCode) { if (jmcData.currencies && jmcData.currencies[currencyCode]) { var symbol = jmcData.currencies[currencyCode].symbol; calculator.find('.jmc-currency-symbol').text(symbol); } } /** * Get current currency from calculator */ function getCurrentCurrency(calculator) { var currencySelect = calculator.find('select[name="currency"]'); if (currencySelect.length) { return currencySelect.val(); } // If no select, get from hidden field or use base currency var hiddenCurrency = calculator.find('input[name="currency"]'); if (hiddenCurrency.length) { return hiddenCurrency.val(); } return jmcData.baseCurrency || 'NGN'; } /** * Show loading state on button */ function showLoading(btn) { btn.prop('disabled', true); btn.find('.jmc-btn-text').hide(); btn.find('.jmc-btn-loading').css('display', 'inline-flex'); } /** * Hide loading state on button */ function hideLoading(btn) { btn.prop('disabled', false); btn.find('.jmc-btn-text').show(); btn.find('.jmc-btn-loading').hide(); } /** * Show toast notification */ function showToast(message, type) { type = type || 'info'; // 'success', 'error', 'info', 'warning' // Remove any existing toasts $('.jmc-toast').remove(); // Create toast element var iconMap = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; var toast = $('
' + '' + iconMap[type] + '' + '' + message + '' + '' + '
'); // Add to body $('body').append(toast); // Trigger animation setTimeout(function() { toast.addClass('jmc-toast-show'); }, 10); // Auto remove after 5 seconds var autoRemove = setTimeout(function() { removeToast(toast); }, 5000); // Manual close toast.find('.jmc-toast-close').on('click', function() { clearTimeout(autoRemove); removeToast(toast); }); } /** * Remove toast with animation */ function removeToast(toast) { toast.removeClass('jmc-toast-show'); setTimeout(function() { toast.remove(); }, 300); } /** * Validate interest rate against minimum */ function validateInterestRate(input) { var minRate = parseFloat(jmcData.minInterestRate) || 0; var value = parseFloat(input.val()) || 0; if (value < minRate) { input.val(minRate); return minRate; } return value; } /** * Initialize Mortgage Calculator */ function initMortgageCalculator() { var $form = $(document).find('#jmc-mortgage-form'); if (!$form.length) { return; } var $calculator = $form.closest('.jmc-calculator'); // Currency input formatting $calculator.on('input', '.jmc-currency-input', function() { formatCurrencyInput($(this)); }); // Currency change $calculator.on('change', '.jmc-currency-select', function() { updateCurrencySymbols($calculator, $(this).val()); }); // Interest rate validation $calculator.on('change', 'input[name="interest_rate"]', function() { validateInterestRate($(this)); }); // Reset button handler $calculator.on('click', '.jmc-reset-btn', function() { $('#jmc-mortgage-results').removeClass('has-results'); }); // Form submission - use delegation $(document).on('submit', '#jmc-mortgage-form', function(e) { e.preventDefault(); e.stopPropagation(); var $thisForm = $(this); var $btn = $thisForm.find('.jmc-calculate-btn'); var $calc = $thisForm.closest('.jmc-calculator'); showLoading($btn); var currency = getCurrentCurrency($calc); var data = { action: 'jmc_calculate_mortgage', nonce: jmcData.nonce, loan_amount: parseCurrencyInput($thisForm.find('input[name="loan_amount"]').val()), interest_rate: validateInterestRate($thisForm.find('input[name="interest_rate"]')), loan_term: parseInt($thisForm.find('input[name="loan_term"]').val()) || 20, currency: currency }; $.ajax({ url: jmcData.ajaxUrl, type: 'POST', data: data, success: function(response) { hideLoading($btn); if (response.success) { displayMortgageResults(response.data); calculationResults.mortgage = response.data; // Store for lead capture $('#jmc-mortgage-calculation-data').val(JSON.stringify(response.data)); } else { showToast(response.data || jmcData.i18n.error, 'error'); } }, error: function(xhr, status, error) { hideLoading($btn); console.error('AJAX Error:', error); showToast(jmcData.i18n.error, 'error'); } }); return false; }); // Reset form $(document).on('reset', '#jmc-mortgage-form', function() { setTimeout(function() { $('#jmc-mortgage-results').hide(); if (charts.mortgagePie) { charts.mortgagePie.destroy(); charts.mortgagePie = null; } }, 10); }); // Lead form submission $(document).on('submit', '#jmc-mortgage-lead-form', function(e) { e.preventDefault(); submitLead($(this), 'mortgage'); return false; }); } /** * Display mortgage calculation results */ function displayMortgageResults(data) { const results = $('#jmc-mortgage-results'); const currency = data.currency; $('#jmc-mortgage-monthly-payment').text(formatCurrency(data.monthly_payment, currency)); $('#jmc-mortgage-total-payment').text(formatCurrency(data.total_payment, currency)); $('#jmc-mortgage-total-interest').text(formatCurrency(data.total_interest, currency)); $('#jmc-mortgage-loan-amount').text(formatCurrency(data.loan_amount, currency)); // Add has-results class to show content and hide empty state results.addClass('has-results'); // Scroll to results on mobile (stacked layout) if ($(window).width() < 992) { $('html, body').animate({ scrollTop: results.offset().top - 20 }, 500); } // Create pie chart createMortgagePieChart(data); } /** * Create mortgage pie chart */ function createMortgagePieChart(data) { const ctx = document.getElementById('jmc-mortgage-chart'); if (!ctx) return; if (charts.mortgagePie) { charts.mortgagePie.destroy(); } const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--jmc-primary').trim() || '#1e3a5f'; const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--jmc-accent').trim() || '#e74c3c'; charts.mortgagePie = new Chart(ctx, { type: 'doughnut', data: { labels: [jmcData.i18n.principal, jmcData.i18n.interest], datasets: [{ data: [data.loan_amount, data.total_interest], backgroundColor: [primaryColor, accentColor], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } }, tooltip: { callbacks: { label: function(context) { return context.label + ': ' + formatCurrency(context.raw, data.currency); } } } } } }); } /** * Nigerian mortgage constants */ var NIGERIAN_RETIREMENT_AGE = 60; var MAX_LOAN_TERM = 25; var MAX_DTI = 33.3; /** * Calculate maximum loan term based on age */ function calculateMaxTerm(age) { var yearsToRetirement = NIGERIAN_RETIREMENT_AGE - age; return Math.min(Math.max(yearsToRetirement, 1), MAX_LOAN_TERM); } /** * Update loan term based on age */ function updateLoanTermFromAge($form) { var age = parseInt($form.find('input[name="age"]').val()) || 35; var maxTerm = calculateMaxTerm(age); var $termInput = $form.find('input[name="loan_term"]'); var currentTerm = parseInt($termInput.val()) || 20; // Update max attribute $termInput.attr('max', maxTerm); // If current term exceeds max, adjust it if (currentTerm > maxTerm) { $termInput.val(maxTerm); } // Update hint text var $hint = $form.find('.jmc-max-term-hint'); if ($hint.length) { $hint.text('Max: ' + maxTerm + ' years (retirement at ' + NIGERIAN_RETIREMENT_AGE + ')'); } return maxTerm; } /** * Initialize Affordability Calculator */ function initAffordabilityCalculator() { var $form = $(document).find('#jmc-affordability-form'); if (!$form.length) return; var $calculator = $form.closest('.jmc-calculator'); // Initialize max term based on default age updateLoanTermFromAge($form); // Currency input formatting $calculator.on('input', '.jmc-currency-input', function() { formatCurrencyInput($(this)); }); // Currency change $calculator.on('change', '.jmc-currency-select', function() { updateCurrencySymbols($calculator, $(this).val()); }); // Age change - update max loan term $calculator.on('change input', 'input[name="age"]', function() { updateLoanTermFromAge($form); }); // Interest rate validation $calculator.on('change', 'input[name="interest_rate"]', function() { validateInterestRate($(this)); }); // Reset button handler $calculator.on('click', '.jmc-reset-btn', function() { $('#jmc-affordability-results').removeClass('has-results'); // Reset max term to default based on default age setTimeout(function() { updateLoanTermFromAge($form); }, 10); }); // Form submission $(document).on('submit', '#jmc-affordability-form', function(e) { e.preventDefault(); e.stopPropagation(); var $thisForm = $(this); var $btn = $thisForm.find('.jmc-calculate-btn'); var $calc = $thisForm.closest('.jmc-calculator'); showLoading($btn); var currency = getCurrentCurrency($calc); var age = parseInt($thisForm.find('input[name="age"]').val()) || 35; var maxTerm = calculateMaxTerm(age); var requestedTerm = parseInt($thisForm.find('input[name="loan_term"]').val()) || 20; var actualTerm = Math.min(requestedTerm, maxTerm); var data = { action: 'jmc_calculate_affordability', nonce: jmcData.nonce, annual_income: parseCurrencyInput($thisForm.find('input[name="annual_income"]').val()), monthly_debts: parseCurrencyInput($thisForm.find('input[name="monthly_debts"]').val()), down_payment: parseCurrencyInput($thisForm.find('input[name="down_payment"]').val()), interest_rate: validateInterestRate($thisForm.find('input[name="interest_rate"]')), loan_term: actualTerm, age: age, currency: currency }; $.ajax({ url: jmcData.ajaxUrl, type: 'POST', data: data, success: function(response) { hideLoading($btn); if (response.success) { displayAffordabilityResults(response.data, maxTerm, requestedTerm); calculationResults.affordability = response.data; // Store for lead capture $('#jmc-affordability-calculation-data').val(JSON.stringify(response.data)); } else { showToast(response.data || jmcData.i18n.error, 'error'); } }, error: function() { hideLoading($btn); showToast(jmcData.i18n.error, 'error'); } }); return false; }); // Reset form $(document).on('reset', '#jmc-affordability-form', function() { setTimeout(function() { $('#jmc-affordability-results').hide(); $('#jmc-afford-term-info').hide(); updateLoanTermFromAge($form); }, 10); }); // Lead form submission $(document).on('submit', '#jmc-affordability-lead-form', function(e) { e.preventDefault(); submitLead($(this), 'affordability'); return false; }); } /** * Display affordability calculation results */ function displayAffordabilityResults(data, maxTerm, requestedTerm) { const results = $('#jmc-affordability-results'); const currency = data.currency; $('#jmc-afford-max-price').text(formatCurrency(data.max_home_price, currency)); $('#jmc-afford-max-loan').text(formatCurrency(data.max_loan_amount, currency)); $('#jmc-afford-down-payment').text(formatCurrency(data.down_payment, currency)); $('#jmc-afford-monthly').text(formatCurrency(data.max_monthly_payment, currency)); // Single DTI bar (Nigerian standard - 33.3%) var dti = data.dti || data.back_end_dti || 0; $('#jmc-afford-dti').text(dti.toFixed(1) + '%'); var dtiBar = $('#jmc-afford-dti-bar'); // Scale bar: 0-50% range mapped to 0-100% width dtiBar.css('width', Math.min(dti, 50) * 2 + '%'); // Add warning/danger classes based on Nigerian 33.3% threshold dtiBar.removeClass('warning danger good'); if (dti <= MAX_DTI) { dtiBar.addClass('good'); } else if (dti <= 40) { dtiBar.addClass('warning'); } else { dtiBar.addClass('danger'); } // Show term adjustment info if term was capped var $termInfo = $('#jmc-afford-term-info'); if (maxTerm && requestedTerm && requestedTerm > maxTerm) { $('#jmc-afford-term-message').text( 'Your loan term has been adjusted from ' + requestedTerm + ' to ' + maxTerm + ' years based on retirement age (' + NIGERIAN_RETIREMENT_AGE + ').' ); $termInfo.show(); } else { $termInfo.hide(); } // Add has-results class to show content and hide empty state results.addClass('has-results'); // Scroll to results on mobile (stacked layout) if ($(window).width() < 992) { $('html, body').animate({ scrollTop: results.offset().top - 20 }, 500); } } /** * Initialize Amortization Calculator */ function initAmortizationCalculator() { var $form = $(document).find('#jmc-amortization-form'); if (!$form.length) return; var $calculator = $form.closest('.jmc-calculator'); // Currency input formatting $calculator.on('input', '.jmc-currency-input', function() { formatCurrencyInput($(this)); }); // Currency change $calculator.on('change', '.jmc-currency-select', function() { updateCurrencySymbols($calculator, $(this).val()); }); // Interest rate validation $calculator.on('change', 'input[name="interest_rate"]', function() { validateInterestRate($(this)); }); // Reset button handler $calculator.on('click', '.jmc-reset-btn', function() { $('#jmc-amortization-results').removeClass('has-results'); }); // Form submission $(document).on('submit', '#jmc-amortization-form', function(e) { e.preventDefault(); e.stopPropagation(); var $thisForm = $(this); var $btn = $thisForm.find('.jmc-calculate-btn'); var $calc = $thisForm.closest('.jmc-calculator'); showLoading($btn); var currency = getCurrentCurrency($calc); var data = { action: 'jmc_calculate_amortization', nonce: jmcData.nonce, loan_amount: parseCurrencyInput($thisForm.find('input[name="loan_amount"]').val()), interest_rate: validateInterestRate($thisForm.find('input[name="interest_rate"]')), loan_term: parseInt($thisForm.find('input[name="loan_term"]').val()) || 20, currency: currency }; $.ajax({ url: jmcData.ajaxUrl, type: 'POST', data: data, success: function(response) { hideLoading($btn); if (response.success) { displayAmortizationResults(response.data); calculationResults.amortization = response.data; // Store for lead capture (without full schedule to keep email size reasonable) var leadData = { monthly_payment: response.data.monthly_payment, total_payment: response.data.total_payment, total_interest: response.data.total_interest, total_principal: response.data.total_principal, currency: response.data.currency }; $('#jmc-amortization-calculation-data').val(JSON.stringify(leadData)); } else { showToast(response.data || jmcData.i18n.error, 'error'); } }, error: function() { hideLoading($btn); showToast(jmcData.i18n.error, 'error'); } }); return false; }); // Chart tabs $(document).on('click', '.jmc-amortization-calculator .jmc-chart-tab', function() { var chart = $(this).data('chart'); var $calc = $(this).closest('.jmc-calculator'); $calc.find('.jmc-chart-tab').removeClass('active'); $(this).addClass('active'); $calc.find('[id^="jmc-amort-chart-"]').hide(); $('#jmc-amort-chart-' + chart).show(); }); // Schedule view toggle $(document).on('click', '.jmc-amortization-calculator .jmc-schedule-view-btn', function() { var view = $(this).data('view'); var $calc = $(this).closest('.jmc-calculator'); $calc.find('.jmc-schedule-view-btn').removeClass('active'); $(this).addClass('active'); if (view === 'yearly') { $('#jmc-amort-schedule-yearly').show(); $('#jmc-amort-schedule-monthly').hide(); } else { $('#jmc-amort-schedule-yearly').hide(); $('#jmc-amort-schedule-monthly').show(); } }); // Reset form $(document).on('reset', '#jmc-amortization-form', function() { setTimeout(function() { $('#jmc-amortization-results').hide(); destroyAmortizationCharts(); }, 10); }); // Lead form submission $(document).on('submit', '#jmc-amortization-lead-form', function(e) { e.preventDefault(); submitLead($(this), 'amortization'); return false; }); } /** * Display amortization results */ function displayAmortizationResults(data) { const results = $('#jmc-amortization-results'); const currency = data.currency; $('#jmc-amort-monthly').text(formatCurrency(data.monthly_payment, currency)); $('#jmc-amort-total').text(formatCurrency(data.total_payment, currency)); $('#jmc-amort-interest').text(formatCurrency(data.total_interest, currency)); $('#jmc-amort-principal').text(formatCurrency(data.total_principal, currency)); // Populate tables populateAmortizationTables(data, currency); // Create charts createAmortizationCharts(data, currency); // Add has-results class to show content and hide empty state results.addClass('has-results'); // Scroll to results on mobile (stacked layout) if ($(window).width() < 992) { $('html, body').animate({ scrollTop: results.offset().top - 20 }, 500); } } /** * Populate amortization tables */ function populateAmortizationTables(data, currency) { // Yearly table const yearlyTbody = $('#jmc-amort-yearly-tbody'); yearlyTbody.empty(); data.yearly_summary.forEach(function(row) { yearlyTbody.append(` ${row.year} ${formatCurrency(row.principal, currency)} ${formatCurrency(row.interest, currency)} ${formatCurrency(row.balance, currency)} `); }); // Monthly table (limited to first 5 years for performance) const monthlyTbody = $('#jmc-amort-monthly-tbody'); monthlyTbody.empty(); const scheduleToShow = data.schedule.slice(0, 60); // First 5 years scheduleToShow.forEach(function(row) { monthlyTbody.append(` ${row.month} ${formatCurrency(row.payment, currency)} ${formatCurrency(row.principal, currency)} ${formatCurrency(row.interest, currency)} ${formatCurrency(row.balance, currency)} `); }); if (data.schedule.length > 60) { monthlyTbody.append(` ... ${data.schedule.length - 60} more months ... `); } } /** * Create amortization charts */ function createAmortizationCharts(data, currency) { destroyAmortizationCharts(); const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--jmc-primary').trim() || '#1e3a5f'; const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--jmc-secondary').trim() || '#2ecc71'; const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--jmc-accent').trim() || '#e74c3c'; // Yearly breakdown chart const yearlyCtx = document.getElementById('jmc-amort-yearly-chart'); if (yearlyCtx) { const years = data.yearly_summary.map(r => 'Year ' + r.year); const principals = data.yearly_summary.map(r => r.principal); const interests = data.yearly_summary.map(r => r.interest); charts.amortYearly = new Chart(yearlyCtx, { type: 'bar', data: { labels: years, datasets: [ { label: jmcData.i18n.principal, data: principals, backgroundColor: primaryColor }, { label: jmcData.i18n.interest, data: interests, backgroundColor: accentColor } ] }, options: { responsive: true, maintainAspectRatio: true, scales: { x: { stacked: true }, y: { stacked: true, ticks: { callback: function(value) { return formatCurrency(value, currency); } } } }, plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': ' + formatCurrency(context.raw, currency); } } } } } }); } // Pie chart const pieCtx = document.getElementById('jmc-amort-pie-chart'); if (pieCtx) { charts.amortPie = new Chart(pieCtx, { type: 'doughnut', data: { labels: [jmcData.i18n.principal, jmcData.i18n.interest], datasets: [{ data: [data.total_principal, data.total_interest], backgroundColor: [primaryColor, accentColor], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: function(context) { return context.label + ': ' + formatCurrency(context.raw, currency); } } } } } }); } // Balance over time chart const balanceCtx = document.getElementById('jmc-amort-balance-chart'); if (balanceCtx) { const balanceYears = data.yearly_summary.map(r => 'Year ' + r.year); const balances = data.yearly_summary.map(r => r.balance); charts.amortBalance = new Chart(balanceCtx, { type: 'line', data: { labels: balanceYears, datasets: [{ label: jmcData.i18n.balance, data: balances, borderColor: primaryColor, backgroundColor: 'rgba(30, 58, 95, 0.1)', fill: true, tension: 0.3 }] }, options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return formatCurrency(value, currency); } } } }, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return jmcData.i18n.balance + ': ' + formatCurrency(context.raw, currency); } } } } } }); } } /** * Destroy amortization charts */ function destroyAmortizationCharts() { if (charts.amortYearly) charts.amortYearly.destroy(); if (charts.amortPie) charts.amortPie.destroy(); if (charts.amortBalance) charts.amortBalance.destroy(); } /** * Submit lead form */ function submitLead(form, calculatorType) { const btn = form.find('.jmc-lead-submit-btn'); showLoading(btn); const formData = form.serializeArray(); const data = { action: 'jmc_submit_lead', nonce: jmcData.nonce, currency: form.closest('.jmc-calculator').find('.jmc-currency-select').val() }; formData.forEach(function(item) { if (item.name === 'calculation_data') { data.calculation_data = JSON.parse(item.value || '{}'); } else { data[item.name] = item.value; } }); $.post(jmcData.ajaxUrl, data, function(response) { hideLoading(btn); if (response.success) { form.hide(); form.siblings('.jmc-lead-success').show(); } else { showToast(response.data || jmcData.i18n.error, 'error'); } }).fail(function() { hideLoading(btn); showToast(jmcData.i18n.error, 'error'); }); } /** * Initialize all calculators */ function init() { initMortgageCalculator(); initAffordabilityCalculator(); initAmortizationCalculator(); } // Initialize on document ready $(document).ready(init); })(jQuery);